-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make sure the HTTP call is never made inside a database transaction. #1099
base: master
Are you sure you want to change the base?
Conversation
…because it calls an endpoint. Signed-off-by: Geoffrey Huck <[email protected]>
…ntext in the DataStore. We need the endpoint to call the async propagation. And it has to be from DataStore because we want to specifically do it after the current transaction. Signed-off-by: Geoffrey Huck <[email protected]>
…lic. Signed-off-by: Geoffrey Huck <[email protected]>
…t does. Move it in the `database` package. Because otherwise we have a cyclic dependence error when calling it from the `database` package (database -> service -> auth -> database). This might be solvable in another way though, but it would require refactoring. Signed-off-by: Geoffrey Huck <[email protected]>
…he calls in services. Signed-off-by: Geoffrey Huck <[email protected]>
…in the slice. It's not a requirement, it would work without it, but it feels better. Also, the endpoint is called with the types, so it avoids redundancy there too. Signed-off-by: Geoffrey Huck <[email protected]>
Signed-off-by: Geoffrey Huck <[email protected]>
…enAndRunListeners`. It makes more sense because the work that needs a propagation is done there already. Signed-off-by: Geoffrey Huck <[email protected]>
…guration in the `request Context`. @see #1099 Signed-off-by: Geoffrey Huck <[email protected]>
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #1099 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 239 239
Lines 14462 14480 +18
=========================================
+ Hits 14462 14480 +18 ☔ View full report in Codecov by Sentry. |
Signed-off-by: Geoffrey Huck <[email protected]>
Signed-off-by: Geoffrey Huck <[email protected]>
Signed-off-by: Geoffrey Huck <[email protected]>
…chedule it asynchronously. Because we expect the database to be propagated after the command ends. Signed-off-by: Geoffrey Huck <[email protected]>
…s is intended because in this situation, it shouldn't have a value. Signed-off-by: Geoffrey Huck <[email protected]>
… propagation. In case no endpoint is defined, like for the tests, it was run after, because in this case the propagation is run in sync. When that happens, some tests don't pass. Signed-off-by: Geoffrey Huck <[email protected]>
…l propagations when one is done, there is no distinction between with and without results propagation anymore. @see #1100 Signed-off-by: Geoffrey Huck <[email protected]>
…l propagations when one is done, the permissions propagation is always done (because the results propagation is always done). @see #1100 Signed-off-by: Geoffrey Huck <[email protected]>
I would prefer that @zenovich reviews this one. |
I still believe that the answer is "no" as it was discussed days before on Slack. The reason is that DataStore is just a clever DB interface decorator. But probably we could rethink the meaning of DataStore. And actually I need to study this a bit more from scratch, as, from my perspective (from what it looked like two years ago), mutating operations on the DB resulted in setting the DB into inconsistent state (because of the operations themselves and also because of MySQL triggers), the propagations fixed that by bringing the DB into the consistent state again. That was the reason of calling the propagations in the same transaction with the mutating operations. So, first I would like to study the current situation. @smadbe, is it an urgent task? |
The main thing: Such situations are really dangerous and hard to debug. They will definitely lead to production failures and DB inconsistency issues. I would think 100500 times before doing this and finally would find another approach. |
s.mustBeInTransaction() | ||
|
||
triggersToRun := s.DB.ctx.Value(triggersContextKey).(*awaitingTriggers) | ||
triggersToRun.SchedulePropagationTypes = utils.UniqueStrings(append(triggersToRun.SchedulePropagationTypes, types...)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the order of types doesn't matter, it would be much cleaner to store map[string]struct{} in triggersToRun.SchedulePropagationTypes instead of creating utils.UniqueStrings() and tests for it. Also, the map works faster. But a struct containing all the possible values as boolean fields (like 'awaitingTriggers') would be even better than the map: it would store only several booleans while the map stores strings. Also, the struct provides immediate access to its fields, while the map uses hashing. Also, with the struct, it is not possible to mistype a value which is an often issue with maps/slices.
If the order of types matters, UniqueString() breaks it anyway since maps don't preserve the order of keys.
@@ -262,10 +279,6 @@ func (s *DataStore) WithForeignKeyChecksDisabled(blockFunc func(*DataStore) erro | |||
}) | |||
} | |||
|
|||
func (s *DataStore) IsInTransaction() bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was a nice useful method.
|
||
return nil | ||
}) | ||
mustNotBeError(err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Public methods cannot return errors via panic, panic/recover pattern should be used only within a package (see https://go.dev/doc/effective_go#recover). Also, it is an "architecture decision" made in March 2019, which we have always been following since then.
// If endpoint is an empty string, it will be done synchronously. | ||
func SchedulePropagation(store *database.DataStore, endpoint string, types []string) { | ||
func StartAsyncPropagation(store *DataStore, endpoint string, types []string) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would move this method into app/service. It can be called right from the endpoint handler in case when the async propagation is chosen. The database package is for database-related things only.
@@ -1,6 +1,8 @@ | |||
package utils |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A package should not be called 'utils' as all the packages should have meaningful names. 'utils' is a classic example of bad package names. We should do something with it (see https://go.dev/blog/package-names)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the above
Make all propagations async
fixes #1081
Because we want to have fast transaction.
Now, we call the endpoint at the end of the current transaction.
Tricky part: How to pass the config endpoint to the
DataStore
That was the trickiest part.
The first question is: Do
DataStore
need to know about how to schedule the propagation? I choose that the answer was yes becauseit was easierthe fact we do it after a transaction means it makes sense to be aware of transactions.Also,
InTransaction
is called in many places, and it doesn't make sense at all to pass the endpoint as a parameter to this function, because most transactions are not about propagations at all.What was done: the
request Context
was already passed to theDataStore
, but it wasn't used. So the endpoint is now stored in therequest Context
, in a middleware, and retrieved from theDataStore
. It's the easiest way I found.Alternatives
InTransactionWithPropagation
with the endpoint as parameter, that does the same asInTransaction
but with a propagation. The downside is that we would have to know which one to use at the start of the transaction. It seems to me that the propagation is a notion that appears at a lower level than the service.Make sure the propagation types are unique when we call the endpoint
We also make the types of propagation, sent as a parameter when we call the propagation endpoint, unique. We don't have to, it would work without it, but it seems cleaner.
Review
Easier to review commit by commit. Details in the commit messages.